package ArithmeticLibrary.Elements;

import java.math.BigInteger;
import java.util.List;

import org.bouncycastle.math.ec.ECPoint;

import ArithmeticLibrary.Sets.EllipticCurve;
import ArithmeticLibrary.Util.Point;

import ByteTree.ByteTree;
import ByteTree.Leaf;
import ByteTree.Node;

/**
 * This class represents a group element derived from an elliptic curve.
 * This class supports group operations: multiplication (and division), power, and multiplicative inverse.
 * 
 * @author Lea Golan
 *
 */
public class ElementEllipticCurve extends ElementGroup{

	private EllipticCurve curve;//a reference to the curve the element belongs to
	private Point val;//the value of the element
	private ECPoint valEC;//the value of the element supported by bouncycastle
	
	/**
	 * constructs a new element elliptic curve according to the parameters
	 * 
	 * @param element - element value
	 * @param containerGroup - the curve the element belongs to
	 * @throws Exception if the point is not on the curve
	 */
	public ElementEllipticCurve(Point element, EllipticCurve containerGroup) throws Exception {
		BuildFromPoint(element, containerGroup);
	}

	/**
	 * constructs a new element elliptic curve according to the parameters
	 * 
	 * @param tree - of the form node(leaf_k(x), leaf_k(y)) where k = ceil(log_2 (p)).
	 * @param containerGroup - the curve the element belongs to
	 * @throws Exception - if tree is not properly formatted
	 */
	public ElementEllipticCurve(ByteTree tree, EllipticCurve containerGroup) throws Exception {
		Point val;
		Node root = tree.getRoot();
		if ((root.isLeaf() == true) || (root.getNumChildren() != 2)){
			throw new Exception("Build error ElementEllipticCurve:\nByteTree structure is inconsistent with ElementEllipticCurve");
		}
		
		List<Node> children = root.getChildren();
		if ((children.get(0).isLeaf() == false) || (children.get(1).isLeaf() == false)){
			throw new Exception("Build error ElementEllipticCurve:\nByteTree structure is inconsistent with ElementEllipticCurve");
		}
		
		BigInteger x = new BigInteger(((Leaf) children.get(0)).getData());
		BigInteger y = new BigInteger(((Leaf) children.get(1)).getData());
		
		val = new Point(x, y);
		BuildFromPoint(val, containerGroup);
	}
	
	/**
	 * constructs a new element elliptic curve according to the parameters
	 * 
	 * @param val - a point on  curve
	 * @param containerGroup - the curve the element belongs to
	 * @throws Exception - if any mathematical operation fails
	 */
	public ElementEllipticCurve(ECPoint val, EllipticCurve containerGroup) throws Exception {
		this.curve = containerGroup;
		this.valEC = val;
		try {
			if (val.isInfinity()){
				this.val = new Point(BigInteger.valueOf(-1), BigInteger.valueOf(-1));
			}
			else {
				this.val = new Point(this.valEC.getX().toBigInteger(), 
									 this.valEC.getY().toBigInteger());
			}
		} catch (Exception e) {
			throw new Exception("BuildError in ElementEllipticCurve:\n"+e.getMessage());
		}
	}

	private void BuildFromPoint(Point element, EllipticCurve containerGroup) throws Exception{
		this.curve = containerGroup;
		
		this.val = element;
		if (isPointOnCurve(element) == false){
			throw new Exception("Build error ElementEllipticCurve.BuildFromPoint: point is not on curve");
		}
		if (this.val.isInfinity() == false){
			try {
				this.val.setX(val.getX().mod(curve.getSuperGroupOrder()));
				this.val.setY(val.getY().mod(curve.getSuperGroupOrder()));
			} catch (Exception e) {
				throw new Exception("Build error ElementEllipticCurve.BuildFromPoint:\n"+e.getMessage());
			}
			this.valEC = new ECPoint.Fp(curve.getECCurve(), 
					curve.getECCurve().fromBigInteger(val.getX()), 
					curve.getECCurve().fromBigInteger(val.getY()));
		}
		else {
			this.valEC = this.curve.getECCurve().getInfinity();
		}
	}
	
	private boolean isPointOnCurve(Point p) {
		if (p.isInfinity()) return true;
		BigInteger squareY = p.getY().multiply(p.getY()).mod(this.curve.getSuperGroupOrder());
		return (this.curve.getY(p.getX()).equals(squareY));
	}
	
	/**
	 * Compare this to obj by checking equivalence of the value and the set.
	 */
	public boolean equals(java.lang.Object object){
		if (object == this){
            return true;
        }
        if (!(object instanceof ElementEllipticCurve)){
            return false;
        }        
        ElementEllipticCurve other = (ElementEllipticCurve) object;
        if (this.isInfinity()){
        	return other.isInfinity();
        }
        if (other.isInfinity()){
        	return this.isInfinity();
        } 
        return ((this.curve.equals(other.curve)) &&
        		(this.val.equals(other.val)) &&
        		(this.valEC.equals(other.valEC)));
	}
	
	/**
	 * Checks if this structure is consistent with obj structure. 
	 * it returns true iff obj is an instance of the same class. 
	 * 
	 * @param obj an object we want to check with our structure
	 * @return true iff this and obj are of the same structure
	 */
	@Override
	public boolean checkStructure(Object obj) {
		return (obj instanceof ElementEllipticCurve);
	}
	
	/**
	 * @return the point the element holds as value
	 */
	public Point getElementValue() {
		return this.val;
	}

	/**
	 * sets the element value to newVal
	 * 
	 * @param newVal - a point on the curve the element belongs to
	 * @throws Exception if newVal is not on the curve
	 */
	public void setElementValue(Point newVal) throws Exception{
		try {
			if (newVal.isInfinity()){
				this.valEC = this.curve.getECCurve().getInfinity();
				return;
			}
			if (isPointOnCurve(newVal) == false){
				throw new Exception("Point is not on curve\n");
			}
			this.val = newVal;
			this.val.setX(this.val.getX().mod(this.curve.getSuperGroupOrder()));
			this.val.setY(this.val.getY().mod(this.curve.getSuperGroupOrder()));
			
			this.valEC = new ECPoint.Fp(curve.getECCurve(), curve.getECCurve().fromBigInteger(val.getX()), curve.getECCurve().fromBigInteger(val.getY()));
		} catch (Exception e) {
			throw new Exception("error in ElementEllipticCurve.setElementValue:\n"+e.getMessage());
		}
	}
	
	/**
	 * @return the group which the element belongs to
	 */
	@Override
	public EllipticCurve getGroup() {
		return this.curve;
	}

	/**
	 * checks if val = (-1,-1)  (and respectively valEC = (null, null) )
	 * 
	 * @return true iff this element represents "point at infinity"
	 */
	public boolean isInfinity() {
		return this.valEC.isInfinity();
	}
	
	/**
	 * Returns a new group element (can be a product or a simple element)
	 * which is the result of calculating this^exponent.
	 * 
	 * @param exponent - an exponent
	 * @return a new ElementEllipticCurve that holds the value this^exponent
	 * @throws Exception if the mathematical operation fails
	 */
	@Override
	public ElementEllipticCurve power(java.math.BigInteger exponent) throws Exception {
		try {
			return new ElementEllipticCurve(this.valEC.multiply(exponent), this.getGroup());
		} catch (Exception e) {
			throw new Exception("error in ElementEllipticCurve.power:\n"+e.getMessage());
		}
	}
	
	/**
	 * multiplies this by the value other holds
	 * 
	 * @param other - the element we multiply by
	 * @return new ElementEllipticCurve which is the result of multiplying the current element with the other element.
	 * @throws Exception if the mathematical operation fails
	 */
	@Override
	public ElementEllipticCurve multiply(ElementGroup other) throws Exception {		
		try{
			ECPoint otherPoint = checkElement(other);
			return new ElementEllipticCurve(this.valEC.add(otherPoint), curve);
		} catch (Exception e) {
			throw new Exception("error in ElementEllipticCurve.multiply:\n"+e.getMessage());
		}
	}

	/**
	 * Returns a new group element which is the result of dividing this element by the other element 
	 * both elements must be in the same group. 
	 * 
	 * @param other - the element we want to divide by
	 * @return new ElementEllipticCurve that is the result of dividing this to other 
	 * @throws Exception if the mathematical operation fails
	 */
	@Override
	public ElementEllipticCurve divide(ElementGroup other) throws Exception {
		try{
			ECPoint otherPoint = checkElement(other);
			return new ElementEllipticCurve(this.valEC.subtract(otherPoint), this.curve);
		} catch (Exception e) {
			throw new Exception("error in ElementEllipticCurve.divide:\n"+e.getMessage());
		}
	}
	
	/**
	 * performs inverse operation on the element value with respect to multiplication
	 * 
	 * @return new ElementEllipticCurve which is the multiplicative inverse of this
	 * @throws Exception if the mathematical operation fails
	 */
	@Override
	public ElementEllipticCurve multInverse() throws Exception {
		try{
			return new ElementEllipticCurve(this.valEC.negate(), this.curve);
		} catch (Exception e) {
			throw new Exception("error in ElementEllipticCurve.multInverse:\n"+e.getMessage());
		}
	}
	
	private ECPoint checkElement(ElementGroup other) throws Exception{
		if (!(other instanceof ElementEllipticCurve)){
			throw new Exception("Element must be a instance of ElementEllipticCurve");
		}
		ElementEllipticCurve otherElem = (ElementEllipticCurve) other;
		if ((this.curve.equals(otherElem.curve)) == false) {
			throw new Exception("Elements must belong to the same group");
		}
		
		return otherElem.valEC;
	}
	
}
